package buildinjava.io;

import buildinjava.BuildException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Copies resources.
 *
 * @author Gili Tzabari
 */
public final class Copy
{
	private final Logger log = LoggerFactory.getLogger(Copy.class);
	private static final int retryCount = 3;
	private static final int retryDelay = 1000;
	private FileFilter stepInto;
	private FileFilter toCopy;

	/**
	 * Creates a new Copy operation.
	 */
	public Copy()
	{
		this.stepInto = FileFilters.isDirectory();
		this.toCopy = FileFilters.isFile();
	}

	/**
	 * Indicates what files should be processed.
	 *
	 * @param stepInto indicates what directories to step into. By default all directories are
	 * included. stepInto does not need to check that the path is actually a directory.
	 * @param toCopy indicates what files should be processed. By default all files are included.
	 * toCopy does not need to check that the path is actually a file.
	 * @return the Copy object
	 * @throws IllegalArgumentException if stepInto or toCopy are null
	 * directories
	 */
	public Copy filter(FileFilter stepInto, FileFilter toCopy) throws IllegalArgumentException
	{
		if (stepInto == null)
			throw new IllegalArgumentException("stepInto may not be null");
		if (toCopy == null)
			throw new IllegalArgumentException("toCopy may not be null");
		this.stepInto = FileFilters.intersection(FileFilters.isDirectory(), stepInto);
		this.toCopy = FileFilters.intersection(FileFilters.isFile(), toCopy);
		return this;
	}

	/**
	 * Copies a file.
	 *
	 * @param source the file to copy
	 * @return an instance of <code>Copy.FromFile</code>
	 * @throws IllegalArgumentException if source is not a file
	 */
	public FromFile fromFile(final File source) throws IllegalArgumentException
	{
		if (!source.isFile())
			throw new IllegalArgumentException("source must be a file: " + source.getAbsolutePath());
		return new FromFile(source, this);
	}

	/**
	 * Copies a directory.
	 *
	 * @param source the directory to copy
	 * @return an instance of <code>Copy.FromDirectory</code>
	 * @throws IllegalArgumentException if source is not a directory
	 */
	public FromDirectory fromDirectory(final File source) throws IllegalArgumentException
	{
		if (!source.isDirectory())
			throw new IllegalArgumentException("source must be a directory: " + source.getAbsolutePath());
		return new FromDirectory(source, this);
	}

	/**
	 * Copies a single file.
	 *
	 * @param source the file to copy from
	 * @param target the file to copy to
	 * @param stepInto indicates what directories to step into. By default all directories are included.
	 * @param toCopy indicates what files should be processed. By default all files are included.
	 * @param log the logger
	 * @throws BuildException if an expected build error occurs
	 */
	private static void copy(File source, File target, FileFilter stepInto, FileFilter toCopy,
													 Logger log)
		throws BuildException
	{
		assert ((source.isFile() && (!target.exists() || target.isFile())) || (source.isDirectory() && (!target.
			exists() || target.isDirectory()))):
			"source=" + source + ", target=" + target;
		if (log.isTraceEnabled())
			log.trace("source = " + source.getAbsolutePath() + ", target = " + target.getAbsolutePath());

		// Degrade gracefully if path changes from a directory to a file at runtime
		boolean sourceIsDirectory = false;
		File[] sourceDirectories = source.listFiles(stepInto);
		if (sourceDirectories != null)
		{
			sourceIsDirectory = true;
			for (File directory: sourceDirectories)
				copy(directory, new File(target, directory.getName()), stepInto, toCopy, log);
		}
		File[] sourceFiles = source.listFiles(toCopy);
		if (sourceFiles != null)
		{
			sourceIsDirectory = true;
			for (File file: sourceFiles)
				copy(file, new File(target, file.getName()), stepInto, toCopy, log);
		}
		if (sourceIsDirectory)
			return;
		if (log.isTraceEnabled())
			log.trace("Copying " + source.getPath() + " to " + target.getPath());
		if (!source.exists())
			throw new BuildException(source.getAbsolutePath() + " does not exist");
		long sourceTime = source.lastModified();
		long targetTime = target.lastModified();
		if (sourceTime <= targetTime)
		{
			if (log.isDebugEnabled())
				log.debug("Skipping unmodified file: " + source.getAbsolutePath());
			return;
		}
		File targetDirectory = target.getParentFile();
		if (!targetDirectory.exists() && !targetDirectory.mkdirs())
			throw new BuildException("Cannot create " + targetDirectory.getPath());

		try
		{
			for (int i = 0; i < retryCount; ++i)
			{
				BufferedInputStream in = null;
				BufferedOutputStream out = null;
				try
				{
					in = new BufferedInputStream(new FileInputStream(source));
					out = new BufferedOutputStream(new FileOutputStream(target));

					byte[] buffer = new byte[1024];
					while (true)
					{
						int count = in.read(buffer);
						if (count == -1)
							break;
						out.write(buffer, 0, count);
					}

					if (log.isDebugEnabled())
						log.debug("Copied " + source.getAbsolutePath() + " to " + target.getAbsolutePath());
				}
				catch (FileNotFoundException e)
				{
					if (i < retryCount - 1)
					{
						log.warn("Retrying copy operation: " + e.getMessage());
						Thread.sleep(retryDelay);
						continue;
					}
					else
						throw new BuildException(e);
				}
				catch (IOException e)
				{
					if (i < retryCount - 1)
					{
						log.warn("Retrying copy operation: " + e.getMessage());
						Thread.sleep(retryDelay);
						continue;
					}
					else
						throw new BuildException(e);
				}
				finally
				{
					try
					{
						if (in != null)
							in.close();
						if (out != null)
							out.close();
					}
					catch (IOException e)
					{
						if (i < retryCount - 1)
						{
							log.warn("Retrying copy operation: " + e.getMessage());
							Thread.sleep(retryDelay);
							continue;
						}
						else
							throw new BuildException(e);
					}
				}
				break;
			}

			boolean timeChanged = false;
			for (int i = 0; i < retryCount; ++i)
			{
				if (target.setLastModified(source.lastModified()))
				{
					timeChanged = true;
					break;
				}
				if (log.isWarnEnabled())
					log.warn("Retrying changing last-modified time of " + target.getAbsolutePath());
				Thread.sleep(retryDelay);
			}
			if (!timeChanged)
				throw new BuildException("Cannot change last-modified time of " + target.getAbsolutePath());
		}
		catch (InterruptedException e)
		{
			throw new BuildException(e);
		}
	}

	@Override
	public String toString()
	{
		return getClass().getName() + "[stepInto=" + stepInto + ", toCopy=" + toCopy + "]";
	}

	/**
	 * Copies from a file.
	 *
	 * @author Gili Tzabari
	 */
	public static final class FromFile
	{
		private final File source;
		private final FileFilter stepInto;
		private final FileFilter toCopy;
		private final Logger log;

		/**
		 * Creates a new FromFile.
		 *
		 * @param source the file to copy from
		 * @param copy the Copy object
		 */
		private FromFile(final File source, Copy copy)
		{
			this.source = source;
			this.stepInto = copy.stepInto;
			this.toCopy = FileFilters.union(copy.toCopy, new FileFilter()
			{
				@Override
				public boolean accept(File file)
				{
					return file.equals(source);
				}
			});
			this.log = copy.log;
		}

		/**
		 * Copies to a file.
		 *
		 * @param target the file to write into
		 * @throws IllegalArgumentException if target is a directory
		 */
		public void toFile(File target) throws IllegalArgumentException
		{
			if (target.isDirectory())
				throw new IllegalArgumentException("target may not be a directory: " + target.
					getAbsolutePath());
			copy(source, target, stepInto, toCopy, log);
		}

		/**
		 * Copies to a directory.
		 *
		 * @param target the directory to copy into
		 * @throws IllegalArgumentException if target is a file
		 */
		public void toDirectory(File target) throws IllegalArgumentException
		{
			if (target.isFile())
				throw new IllegalArgumentException("target may not be a file: " + target.getAbsolutePath());
			if (!target.exists() && !target.mkdirs())
				throw new BuildException("Cannot create " + target.getPath());
			copy(source, new File(target, source.getName()), stepInto, toCopy, log);
		}
	}

	/**
	 * Copies from a directory.
	 *
	 * @author Gili Tzabari
	 */
	public static final class FromDirectory
	{
		private final File source;
		private final FileFilter stepInto;
		private final FileFilter toCopy;
		private final Logger log;

		/**
		 * Creates a new FromDirectory.
		 *
		 * @param source the directory to copy from
		 * @param copy the Copy object
		 */
		private FromDirectory(final File source, Copy copy)
		{
			this.source = source;
			this.stepInto = copy.stepInto;
			this.toCopy = FileFilters.union(copy.toCopy, new FileFilter()
			{
				@Override
				public boolean accept(File file)
				{
					return file.equals(source);
				}
			});
			this.log = copy.log;
		}

		/**
		 * Copies to a directory.
		 *
		 * @param target the directory to copy into
		 */
		public void toDirectory(File target)
		{
			copy(source, target, stepInto, toCopy, log);
		}
	}
}
